package orchestration.android;

import java.util.ArrayList;
import java.util.HashSet;

import orchestration.android.execution.AbstractBPEService;
import orchestration.android.execution.AndroidCallback2BPExecutionListenerAdapter;
import orchestration.android.execution.AndroidLockableExecution;
import orchestration.android.execution.DTOFactory;
import orchestration.android.execution.IOrchestrationCache;
import orchestration.android.execution.exceptions.LockException;
import orchestration.android.parcelable.ParcelableAndroidTaskDTO;
import orchestration.android.parcelable.ParcelableProperty;
import orchestration.businessprocess.execution.BPExecution;
import orchestration.businessprocess.model.BPNodeId;
import orchestration.businessprocess.model.BusinessProcessModel;
import orchestration.businessprocess.model.nodes.BPNode;
import orchestration.persistence.OrchestratorAuthority;
import orchestration.persistence.OrchestratorAuthority.AndroidProcess;
import orchestration.persistence.OrchestratorAuthority.AndroidProcessExecution;
import orchestration.persistence.exceptions.OrchestrationDAOException;
import orchestration.persistence.sqlite.OrchestratorDatabaseHelper;
import orchestration.utils.ConversionUtils;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

/**
 * a service that expose an API to manage Executions and Properties
 * @author  <a href="mailto:car.ferrarini@gmail.com">Carlo Ferrarini</a>
 */
public class BPEServiceImpl extends AbstractBPEService {
	
	private static final String TAG= "BPEServiceImpl";//current service to use
	
	private static final UriMatcher sUriMatcher;
	private static final int PROCESS_ID=1;
	private static final int EXECUTION_ID=2;
	
	/**
	 * {@link IBusinessProcess} Stub, simply delegates operations to the service
	 * @uml.property  name="binder"
	 * @uml.associationEnd  
	 */
	private IBusinessProcess.Stub binder= new IBusinessProcess.Stub() {

		@Override
		public void execute(Uri bpiUri, String key,boolean repeatLast) throws RemoteException {
			executeImpl(bpiUri, key,repeatLast);
		}

		@Override
		public ParcelableProperty getProperty(Uri bpeUri,String artifactId) throws RemoteException {
			return getPropertyImpl(bpeUri, artifactId);
		}

		@Override
		public void registerForExecution(Uri bpeUri, ICallback callback)
				throws RemoteException {
			registerForExecutionImpl(bpeUri,callback);
		}

		@Override
		public void releaseExecution(Uri bpeUri, String key) throws RemoteException {
			releaseExecutionImpl(bpeUri, key);
		}

		@Override
		public void saveProperty(Uri bpeUri, String key,
				ParcelableProperty property) throws RemoteException {
			savePropertyImpl(bpeUri,key,property);
		}

		@Override
		public ParcelableAndroidTaskDTO getLastNodeInfo(Uri executionUri,
				String key) throws RemoteException {
			return getLastNodeInfoImpl(executionUri,key);
		}

	};
	

	@Override
	public IBinder onBind(Intent intent) {
		// can bind to this service only if working with executions 
		Uri uri= intent.getData();
		if(sUriMatcher.match(uri)==EXECUTION_ID){
			return binder;
		}
		return null;
			
	}
	

	@Override
	public boolean onUnbind(Intent intent) {
		Uri uri= intent.getData();
		if(sUriMatcher.match(uri)==EXECUTION_ID){
			new AsyncTask<Uri, Void, Void>(){
				@Override
				protected Void doInBackground(Uri... params) {
					saveBpe(getAndroidLockableExecution(params[0]));
					return null;
				}
			}.execute(uri);
		}
		return super.onUnbind(intent);
	}
	/**
	 * retrieves a DTO containing all the information attached to
	 * the last node visited by this execution
	 * @param executionUri the {@link Uri} to identify the execution
	 * @param key the locking key
	 * @return the retrieved DTO
	 */
	private ParcelableAndroidTaskDTO getLastNodeInfoImpl(Uri executionUri,
			String key) {
		BPNode node;
		try {
			AndroidLockableExecution ale = getAndroidLockableExecution(executionUri);
			node= ale.getCurrentNode(key);
			ParcelableAndroidTaskDTO dto = DTOFactory.createDTO(node, ale.getExecution());
			return dto;
		} catch (LockException e) {
			handleException(e);
			return null;
		}
		
	}
	/**
	 * saves the property passed as parameter, attaching it to the
	 * execution identified by executionUri
	 * @param executionUri the {@link Uri} to identify the execution
	 * @param key the locking key used
	 * @param property the property to save
	 */
	private void savePropertyImpl(Uri executionUri, String key,
			ParcelableProperty property) {
		try {
			getAndroidLockableExecution(executionUri).saveProperty(key,property);
		} catch (LockException e) {
			handleException(e);
		}
	}

	/**
	 * tries to lock the {@link AndroidLockableExecution} identified by executionUri,
	 * attaches a listener to the execution so that a callback methods will be called while
	 * running the execution
	 * @param executionUri the {@link Uri} to identify the {@link AndroidLockableExecution}
	 * @param callback the callback used to notify back the user
	 */
	private void registerForExecutionImpl(Uri executionUri,final ICallback callback) {
		new AsyncTask<Uri, Void, AndroidLockableExecution>(){//spawn an asynch background thread to retrieve the execution
			@Override
			protected AndroidLockableExecution doInBackground(Uri... params) { return getAndroidLockableExecution(params[0]);}
			
			protected void onPostExecute(AndroidLockableExecution ale) {//in the main thread, lock the retrieved execution and send registrationSucceded notification
				try {
					String key= ale.lock();
					ale.setListener(key,new AndroidCallback2BPExecutionListenerAdapter(callback));
					callback.onRegistrationSucceded(key);
				} catch (LockException e) {
					handleException(e);
				} catch (RemoteException e) {
					handleException(e);
				}
			}
		}.execute(executionUri);
	}
	/**
	 * request to execute the {@link AndroidLockableExecution} identified by executionUri
	 * @param executionUri
	 * @param key the key used to lock the {@link AndroidLockableExecution}
	 * @param repeatLast true if the current task of the execution has to be executed again
	 */
	private void executeImpl(Uri executionUri, final String key,final boolean repeatLast){
		if(sUriMatcher.match(executionUri)==EXECUTION_ID){
			new AsyncTask<Uri, Void, AndroidLockableExecution>(){
				@Override
				protected AndroidLockableExecution doInBackground(Uri... params) { return getAndroidLockableExecution(params[0]);}
				
				protected void onPostExecute(AndroidLockableExecution newBpi) {
					try {
						newBpi.resume(key,repeatLast);
					} catch (LockException e) {
						handleException(e);
					}
				}
			}.execute(executionUri);
		}
	}
	/**
	 * unlocks {@link AndroidLockableExecution} identified by executionUri, using the given key
	 * @param executionUri
	 * @param key
	 */
	private void releaseExecutionImpl(Uri executionUri,final String key){
		if(sUriMatcher.match(executionUri)==EXECUTION_ID){
			new AsyncTask<Uri, Void, AndroidLockableExecution>(){//spawn an asynch background thread to retrieve the execution
				@Override
				protected AndroidLockableExecution doInBackground(Uri... params) { return getAndroidLockableExecution(params[0]);}
				
				protected void onPostExecute(AndroidLockableExecution ale) {
					try {
						ale.setListener(key, null);
						ale.unlock(key);
						saveBpe(ale);
					} catch (LockException e) {
						handleException(e);
					}
				}
			}.execute(executionUri);
		}
		
	}
	/**
	 * 
	 * @param uri
	 * @return
	 */
	private AndroidLockableExecution getAndroidLockableExecution(Uri uri) {
		AndroidLockableExecution ale;
		IOrchestrationCache cache = getCache();
		synchronized (cache) {
			ale = cache.getLockableExecution(uri);			
		}
		if(ale==null){
			Log.w(TAG,"bpe==null, loading from db");
			Cursor bpiC= getContentResolver().query(uri, OrchestratorDatabaseHelper.EXECUTION_FIELDS, null, null, null);
			bpiC.moveToFirst();
			long id= bpiC.getLong(bpiC.getColumnIndex(AndroidProcessExecution._ID));
			long procRef= bpiC.getLong(bpiC.getColumnIndex(AndroidProcessExecution.PROCESS_REF));
			String status=bpiC.getString(bpiC.getColumnIndex(AndroidProcessExecution.EXECUTION_STATUS));
			String lastNodeId=bpiC.getString(bpiC.getColumnIndex(AndroidProcessExecution.CURRENT));
			String[] toVisitStringArray= bpiC.getString(bpiC.getColumnIndex(AndroidProcessExecution.TOVISIT)).split(" ");
			String[] visitedStringArray = bpiC.getString(bpiC.getColumnIndex(AndroidProcessExecution.VISITED)).split(" ");
			String key = bpiC.getString(bpiC.getColumnIndex(AndroidProcessExecution.KEY));
			bpiC.close();
			ArrayList<BPNodeId> toVisitIds = ConversionUtils.stringArray2NodeIdList(toVisitStringArray);
			HashSet<BPNodeId> visitedIds = ConversionUtils.stringArray2NodeIdSet(visitedStringArray);
			BPNodeId currentNodeId= new BPNodeId(lastNodeId);
			Uri procUri= Uri.withAppendedPath(AndroidProcess.CONTENT_URI, ""+procRef);
			BusinessProcessModel bpm= cache.getBpm(procUri);
			if(bpm==null){
				try {
					bpm=getBusinessProcessModelDAO().getBusinessProcessModel(procUri, this);
				} catch (OrchestrationDAOException e) {
					handleException(e);
				}
			}
			BPExecution execution = new BPExecution();
			execution.setId(id);
			execution.setModel(bpm);
			execution.setContainer(this);
			execution.setExecutionStatus(status);
			execution.init(currentNodeId, toVisitIds, visitedIds);
			ale = new AndroidLockableExecution(execution,key);
			synchronized (cache) {
				cache.addBPM(procUri, bpm);
				cache.addLockableExecution(uri, ale);				
			}
		}
		return ale;
	}

	private void saveBpe(AndroidLockableExecution ale) {
		BPExecution execution = ale.getExecution();
		String stackString= ConversionUtils.list2string(execution.getNodesToVisit());
		String visitedString= ConversionUtils.set2string(execution.getVisited());
		String execStatus= execution.getExecutionStatus();
		String lastNode=null;
		long id= execution.getId();
		try{
			lastNode=execution.getCurrentNode().getId().getValue();
		}catch(NullPointerException e){
			lastNode="";
		}
		String encryptedKey = ale.getEncryptedKey();
		ContentValues cv= new ContentValues();
		//other infos are not modifiable
		cv.put(AndroidProcessExecution.TOVISIT, stackString);
		cv.put(AndroidProcessExecution.VISITED, visitedString);
		cv.put(AndroidProcessExecution.EXECUTION_STATUS, execStatus);
		cv.put(AndroidProcessExecution.CURRENT, lastNode);
		cv.put(AndroidProcessExecution.KEY, encryptedKey);
		
		Uri uri=Uri.withAppendedPath(AndroidProcessExecution.CONTENT_URI,""+id);
		getContentResolver().update(uri, cv, null, null);
	}
	static {
		sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		sUriMatcher.addURI(OrchestratorAuthority.AUTHORITY, "processes/#", PROCESS_ID);
		sUriMatcher.addURI(OrchestratorAuthority.AUTHORITY, "process_executions/#", EXECUTION_ID);
	}
	private ParcelableProperty getPropertyImpl(Uri uri, String artifactName) {
		return (ParcelableProperty)getAndroidLockableExecution(uri).getExecution().getProperty(artifactName);
	}
}